"""工具包，放一些通用函数"""
import math
import socket
import logging
import json
import threading

sem = threading.Semaphore(0)
IPCONFIG_PATH = 'ipconfig.json'
logger = logging.getLogger('logger')

ip_cfg: dict = {}
fp = open(IPCONFIG_PATH, 'a+')
fp.seek(0)
try:
    ip_cfg = json.load(fp)
except json.decoder.JSONDecodeError:
    # json文件为空或损坏
    fp.truncate(0)


class ProtocolProcessor:
    """负责协议的解析和封装操作"""
    # 版本号，暂定为0x00
    VERSION = b'\x00'
    # HITSZ_PROTOCOL协议的魔数
    HITSZ_MAGIC_NUM = b'\xA1\x3A'
    # EZ_PROTOCOL协议的魔数
    EZ_MAGIC_NUM = b'\x17'
    # 烧录报文,仅在服务器发送给node时有效，表示data中存放了bit文件需要烧录，此时的operand字段表示连接到哪个FPGA
    COMMAND_FLUSH = b'\x10'
    # node传给服务器的报文，operand中存放展示信息，若operand不够存放则使用拓展报文
    COMMAND_DISPLAY = b'\x20'
    # 烧录进度,本项目中node使用该指令指示fpga烧录完毕
    COMMAND_PROGRESS = b'\x30'
    # 说明该报文存放了拨码开关等的电平信息，按下用1表示
    COMMAND_OPERATE = b'\x40'
    # 测试报文,node收到该消息可以只打印到控制台解析
    COMMAND_TEST = b'\x50'
    # 扩展报文，表示由于operand位数不够占用了部分data段。
    COMMAND_EXTEND = b'\x04'
    # 结束，相当于TCP的fin
    COMMAND_CLOSE = b'\x08'

    def __init__(self, tag: bytes):
        self.tag = tag

    def decode_hitsz(self, code: bytearray):
        """解析服务器传的指令，遵守HITSZProtocol协议"""
        if code[0:2] != self.HITSZ_MAGIC_NUM:
            logger.warning(f'wrong magic_number {code[0:2]}, fpga magic num is {self.HITSZ_MAGIC_NUM}')
            return 'error', 0
        version = code[2:3]
        if version != self.VERSION:
            logger.warning(f'version is {version}')
        tag = code[4:6]
        if tag != self.tag:
            logger.debug(code.hex(" "))
            logger.warning(f'wrong fpga tag {tag}, fpga tag is {self.tag}')
            return 'error', 0
        command = code[3:4]
        if command == self.COMMAND_FLUSH:
            return 'flush', int.from_bytes(code[6:10], byteorder='big', signed=False)
        elif command == self.COMMAND_DISPLAY:
            return 'display', 0
        elif command == self.COMMAND_OPERATE:
            return 'operate', code[10:14]
        elif command == self.COMMAND_CLOSE:
            return 'close', 0
        elif command == self.COMMAND_TEST:
            return 'test', int.from_bytes(code[6:10], byteorder='big', signed=False)
        else:
            logger.debug(f'undefined code: {code}')
            return 'undefined', 0

    def decode_ez(self, code: bytearray):
        """解析服务器传的指令，遵守EZProtocol协议"""
        if code[0:1] != self.EZ_MAGIC_NUM:
            logger.warning(f'wrong magic_number {code[0:1]}, fpga magic num is {self.EZ_MAGIC_NUM}')
            return 'error', 0
        command = code[1:2]
        if command == self.COMMAND_DISPLAY:
            return 'display', 0
        elif command == self.COMMAND_OPERATE:
            return 'operate', code[2:6]
        elif command == self.COMMAND_CLOSE:
            return 'close', 0
        else:
            logger.debug(f'undefined code: {code}')
            return 'undefined', 0

    def decode(self, code: bytearray, sock: socket.socket):
        """判断是ez还是hitsz协议，并返回解析结果"""
        if code[0:1] == self.EZ_MAGIC_NUM:
            return self.decode_ez(code)
        elif code[0:2] == self.HITSZ_MAGIC_NUM:
            b = sock.recv(14 - 6)
            c = bytearray(bytes(code) + b)
            return self.decode_hitsz(c)
        else:
            logger.debug(f'undefined code: {code}')
            return 'undefined', 0

    def encode_hitsz(self, command: bytes, data: bytes, length: int, operand: bytes = b'\x00\x00\x00\x00'):
        code = self.HITSZ_MAGIC_NUM + self.VERSION + command + self.tag + \
               length.to_bytes(4, 'big', signed=False) + operand + data
        return code

    def encode_ez(self, command: bytes, operand: bytes = b'\x00\x00\x00\x00', data: bytes = b''):
        if len(command) != 1:
            return b''
        code = self.EZ_MAGIC_NUM + command + operand + data
        return code


class Converter:
    """
    该类将buffer中的数据转化为展示数据
    """

    def __init__(self, io: dict):
        self.switch = io['switch']
        self.button = io['button']
        self.keyboard = io['keyboard']
        self.led = io['led']
        self.seg_segment = io['seg']['segment']
        self.seg_bit = io['seg']['bit']
        self.seg_enabling_way = io['seg']['enabling_way']

        self.display_data_bits = self.led + self.seg_segment * self.seg_bit
        self.sample_data_bytes = math.ceil((self.led + self.seg_segment + self.seg_bit) / 8)
        """采样数据所需的字节数"""

        self.prevOut: bytearray = bytearray(math.ceil(self.display_data_bits / 8))
        self.curOut: bytearray = bytearray(len(self.prevOut))
        self.cnt: list = [0] * self.display_data_bits

    def convert_to_display_data(self, buf: bytearray):
        """将缓存到的数据转化为展示数据"""
        for i in range(len(self.cnt)):
            self.cnt[i] = 0
        for i in range(len(self.curOut)):
            self.curOut[i] = 0
        interval = 32 * 1000 * self.sample_data_bytes // len(buf)
        cumulate(self.cnt, buf, self.seg_enabling_way, self.seg_segment, self.seg_bit, self.led, self.sample_data_bytes)
        # print(self.cnt)
        for i in range(self.seg_bit * self.seg_segment):
            if interval * self.cnt[i] >= 3000 * self.sample_data_bytes / 5:
                self.curOut[i // 8] |= (1 << (7 - (i % 8)))
        for i in range(self.seg_bit * self.seg_segment, self.seg_bit * self.seg_segment + self.led):
            if interval * self.cnt[i] >= 6000 * self.sample_data_bytes / 5:
                self.curOut[i // 8] |= (1 << (7 - (i % 8)))

    def copy_to_prev(self):
        """令prevOut为curOut"""
        length = len(self.prevOut)
        for i in range(length):
            self.prevOut[i] = self.curOut[i]

    def cur_equal_prev(self) -> bool:
        """判断curOut和prevOut是否相等，相等返回True"""
        length = len(self.prevOut)
        for i in range(length):
            if self.curOut[i] != self.prevOut[i]:
                return False
        return True

    def clear_prev_out(self):
        length = len(self.prevOut)
        for i in range(length):
            self.prevOut[i] = 0


def cumulate(cnt: list, buffer: bytearray, seg_enabling_way: bool, seg_segment: int, seg_bit: int, led: int,
             sample_data_bytes: int):
    """
    对buffer中的数据进行计数，这里做简化考虑：seg_segment在第一个字节，seg_bit在第二个字节，led在后面几个字节。没有跨字节现象出现。
    TODO 这种简化考虑适用于minisys板和ego1板，如考虑普适性需要重新设计本函数。
    """
    size = len(buffer)
    seg_id = -1
    # 累计seg
    for i in range(0, size, sample_data_bytes):
        b = buffer[i + 1]
        if not seg_enabling_way:
            b = (b ^ -1) & 0xff
        c = 0x80
        for j in range(8):
            if b == c:
                seg_id = j
                break
            else:
                c = c >> 1
        if 0 <= seg_id <= 7:
            idx = 8 * seg_id
            # 如果是低电平使能则取反
            if not seg_enabling_way:
                buffer[i] = (buffer[i] ^ -1) & 0xff
            for j in range(8):
                cnt[idx + j] += ((buffer[i] & (0x80 >> j)) >> (7 - j))
        # 累计led
        for j in range(2, sample_data_bytes):
            for k in range(8):
                cnt[64 + (j - 2) * 8 + k] += ((buffer[i + j] & (0x80 >> k)) >> (7 - k))


def get_ip():
    """获取局域网ip，支持校园网"""
    st = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # noinspection PyBroadException
    try:
        st.connect(('10.0.0.0', 1))
        ip = st.getsockname()[0]
    except Exception:
        ip = '127.0.0.1'
    finally:
        st.close()
    return ip


PORT = 50000


def check_port_available(port: int, ip: str) -> bool:
    """检查端口是否被占用，被占用返回True，否则返回False"""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(0.5)
    try:
        logger.debug((ip, port))
        s.bind((ip, port))
    except OSError:
        return False
    finally:
        s.close()
    return True
    # noinspection PyBroadException
    # ret = s.connect_ex((ip, port))
    # logger.info(f'{ret} {(ip, port)}')
    # if ret == 0:
    #     s.close()
    #     return True
    # else:
    #     s.close()
    #     return False


def get_port(ip: str, usb_serial: str) -> int:
    """
    1. 首先尝试在ipconfig文件中获取到端口
    2. 若获取不到，或被占用，尝试PORT
    3. 若PORT不可用，返回0
    """
    global ip_cfg
    global PORT
    if usb_serial in ip_cfg:
        port = ip_cfg[usb_serial]["port"]
        if check_port_available(port, ip):
            return port
    PORT += 1
    if check_port_available(PORT, ip):
        return PORT
    else:
        return 0


def dump_ipconfig():
    global fp
    fp.seek(0)
    fp.truncate(0)
    global ip_cfg
    logger.info(f'dump ipconfig.json')
    json.dump(ip_cfg, fp)
    fp.close()
